package top.wshape1.shiziapi.search.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import com.alibaba.fastjson2.JSON;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import top.wshape1.shiziapi.common.constant.ElasticConstant;
import top.wshape1.shiziapi.common.domain.ProjectSearch;
import top.wshape1.shiziapi.search.domain.ProjectSearchPage;
import top.wshape1.shiziapi.search.service.ProjectSearchService;

import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * @author Wshape1
 * @version 1.0
 * @description
 * @since 2023-11-26
 */

@Service
@Slf4j
public class ProjectSearchServiceImpl implements ProjectSearchService {

    @Resource
    private ElasticsearchClient elasticsearchClient;

    @Override
    public void save(ProjectSearch project) throws IOException {
        BooleanResponse exists = elasticsearchClient.exists(builder -> builder
                .index(ElasticConstant.PROJECT_INDEX_NAME)
                .id(project.getId().toString())
        );

        if (exists.value()) {
            elasticsearchClient.update(builder -> builder
                            .index(ElasticConstant.PROJECT_INDEX_NAME)
                            .id(project.getId().toString())
                            .doc(project)
                            .retryOnConflict(3)
                    , ProjectSearch.class);
        } else {
            elasticsearchClient.create(builder -> builder
                    .index(ElasticConstant.PROJECT_INDEX_NAME)
                    .id(project.getId().toString())
                    .document(project)
            );
        }

    }

    @Override
    public void remove(Long id) throws IOException {
        elasticsearchClient.delete(builder -> builder
                .index(ElasticConstant.PROJECT_INDEX_NAME)
                .id(id.toString())
        );
    }

    @Override
    public void setLike(Long id, Long count) throws IOException {
        ProjectSearch projectSearch = new ProjectSearch();
        projectSearch.setId(id);
        projectSearch.setLikeCount(id);
        this.save(projectSearch);
    }

    @Override
    public void plusOrReduceLike(Long id, String plusOrReduce, Date updateTime) throws IOException {

        elasticsearchClient.update(builder -> builder
                        .index(ElasticConstant.PROJECT_INDEX_NAME)
                        .id(id.toString())
                        .script(builder1 -> builder1
                                .inline(builder2 -> builder2
                                        .lang("painless")
                                        .source("ctx._source.likeCount += params.count; " +
                                                "ctx._source.updateTime = params.updateTime")
                                        .params(Map.of("count", JsonData.fromJson(plusOrReduce.equals("plus") ? "1" : "-1"),
                                                "updateTime", JsonData.fromJson(JSON.toJSONString(updateTime))))
                                )
                        )
                , ProjectSearch.class
        );

    }

    @Override
    public void plusRead(Long id, Date updateTime) throws IOException {
        elasticsearchClient.update(builder -> builder
                        .index(ElasticConstant.PROJECT_INDEX_NAME)
                        .id(id.toString())
                        .script(builder1 -> builder1
                                .inline(builder2 -> builder2
                                        .lang("painless")
                                        .source("ctx._source.readCount += params.one;" +
                                                "ctx._source.updateTime = params.updateTime")
                                        .params(Map.of("one", JsonData.fromJson("1"),
                                                "updateTime", JsonData.fromJson(JSON.toJSONString(updateTime))))
                                )
                        )
                , ProjectSearch.class
        );
    }

    @Override
    public ProjectSearchPage getHotProjects(int page) throws IOException {

        int pageSize = ElasticConstant.PROJECT_DISCOVER_PAGE_SIZE;

        SearchRequest searchRequest = new SearchRequest.Builder()
                .index(ElasticConstant.PROJECT_INDEX_NAME)
                .query(builder1 -> builder1
                        .scriptScore(ss -> ss
                                .script(sss -> sss.inline(si -> si
                                        .params(Map.of(
                                                "likeScore", JsonData.fromJson("5"),
                                                "readScore", JsonData.fromJson("1")
                                        ))
                                        .source("params.readScore * doc['readCount'].value " +
                                                "+ params.likeScore * doc['likeCount'].value")
                                ))
                                .query(q -> q.matchAll(m -> m))
                        )
                )
                .sort(builder1 -> builder1
                                .score(s -> s.order(SortOrder.Desc))
//                                        .field(builder2 -> builder2
//                                                .field("likeCount")
//                                                .order(SortOrder.Desc)
//                                        )
                )
                .size(pageSize)
                .from(pageSize * (page - 1))
                .build();

        log.info(String.valueOf(searchRequest));

        SearchResponse<ProjectSearch> response = elasticsearchClient.search(searchRequest, ProjectSearch.class);

        log.info(String.valueOf(response));

        return getProjectSearchPage(
                response
                        .hits()
                        .hits()
                        .stream()
                        .map(Hit::source)
                        .toList(),
                page,
                null
        );


    }

    @Override
    public ProjectSearchPage searchProject(int page, String keyword) throws IOException {

        if (keyword == null || keyword.isEmpty()) {
            return this.getHotProjects(page);
        }

        int pageSize = ElasticConstant.PROJECT_DISCOVER_PAGE_SIZE;

        SearchResponse<ProjectSearch> response = elasticsearchClient.search(builder -> builder
                        .index(ElasticConstant.PROJECT_INDEX_NAME)
                        .query(builder1 -> builder1
                                .multiMatch(builder2 -> builder2
                                        .fields("name.keyword", "description.keyword", "id.keyword", "userName.keyword")
                                        .query(keyword)
                                )
                        )
                        .sort(builder1 -> builder1
                                .field(builder2 -> builder2
                                        .field("likeCount")
                                        .order(SortOrder.Desc)
                                )
                        )
                        .size(pageSize)
                        .from(pageSize * (page - 1))
                , ProjectSearch.class);

        return getProjectSearchPage(
                response
                        .hits()
                        .hits()
                        .stream()
                        .map(Hit::source)
                        .toList(),
                page,
                keyword
        );
    }

    @Override
//    public List<ProjectSearch> getProjectBetweenUpdateTime(int page, int size, Date startTime, Date endTime) throws IOException {
    public List<ProjectSearch> getProjectBetweenUpdateTime(Date startTime, Date endTime) throws IOException {

        SearchRequest searchRequest = new SearchRequest.Builder()
                .index(ElasticConstant.PROJECT_INDEX_NAME)
                .query(builder1 -> builder1
                        .bool(builder2 -> builder2
                                .must(builder3 -> builder3
                                        .script(bs -> bs.script(bss -> bss.inline(bsi -> bsi
                                                .source("""
                                                                      SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                                                                      long updateTime = format.parse(doc['updateTime.keyword'].value).getTime();
                                                                      if (params.startTime != null && params.endTime != null) {
                                                                            return updateTime >= format.parse(params.startTime).getTime() &&
                                                                                    updateTime <= format.parse(params.endTime).getTime()
                                                                        } else if (params.startTime != null) {
                                                                            return updateTime >= format.parse(params.startTime).getTime()
                                                                        } else if (params.endTime != null) {
                                                                            return updateTime <= format.parse(params.endTime).getTime()
                                                                        } else {
                                                                            return true
                                                                        }
                                                        """)
                                                .params(Map.of(
                                                        "startTime", JsonData.fromJson(JSON.toJSONString(startTime)),
                                                        "endTime", JsonData.fromJson(JSON.toJSONString(endTime))
                                                ))
                                        )))
                                )
                        )
                )
                .build();

        log.info(String.valueOf(searchRequest));

        SearchResponse<ProjectSearch> response = elasticsearchClient.search(searchRequest, ProjectSearch.class);
        return response
                .hits()
                .hits()
                .stream()
                .map(Hit::source)
                .toList();
    }

    @Override
    public long countProject(String keyword) throws IOException {

        if (keyword == null || keyword.isEmpty()) {
            return elasticsearchClient.count(builder -> builder
                    .index(ElasticConstant.PROJECT_INDEX_NAME)
                    .query(builder1 -> builder1
                            .matchAll(builder2 -> builder2)
                    )
            ).count();
        } else {
            return elasticsearchClient.count(builder -> builder
                    .index(ElasticConstant.PROJECT_INDEX_NAME)
                    .query(builder1 -> builder1
                            .multiMatch(builder2 -> builder2
                                    .fields("name.keyword", "description.keyword", "id.keyword", "userName.keyword")
                                    .query(keyword)
                            )
                    )
            ).count();
        }
    }

    @Override
    public ProjectSearch getByProjectId(Long projectId) throws IOException {

        SearchResponse<ProjectSearch> response = elasticsearchClient.search(builder -> builder
                        .index(ElasticConstant.PROJECT_INDEX_NAME)
                        .query(builder1 -> builder1
                                .match(m -> m
                                        .field("projectId")
                                        .query(projectId)
                                )
                        )
                        .size(1)
                , ProjectSearch.class);

        return response.hits().hits().get(0).source();
    }

    private ProjectSearchPage getProjectSearchPage(List<ProjectSearch> projectSearchList, int page, String keyword) throws IOException {

        return new ProjectSearchPage(
                page,
                ElasticConstant.PROJECT_DISCOVER_PAGE_SIZE,
                this.countProject(keyword),
                projectSearchList
        );

    }


}
